Skip to content

feat(realtime): support external offsets and shredding-aware reads - #269

Open
lxy-9602 wants to merge 9 commits into
apache:mainfrom
lxy-9602:refactor-rt
Open

feat(realtime): support external offsets and shredding-aware reads#269
lxy-9602 wants to merge 9 commits into
apache:mainfrom
lxy-9602:refactor-rt

Conversation

@lxy-9602

@lxy-9602 lxy-9602 commented Aug 31, 2026

Copy link
Copy Markdown
Member

Purpose

Linked issue: #158

This change improves realtime writes and reads.

For realtime-enabled writes, external engines must provide a leading non-nullable INT64 _REALTIME_OFFSET:

[_REALTIME_OFFSET, table write fields...]

Offsets must be strictly increasing for each partition-bucket, but gaps are allowed. Paimon persists the exclusive committed offset in the snapshot offset file for WAL recovery. _REALTIME_OFFSET is not written into data files.

Because offsets may be sparse, OffsetRange remains left-closed and right-open but no longer represents the row count.

This PR also moves offset filtering and physical-to-logical conversion into a shared Paimon read pipeline for append-only and primary-key tables. It supports consistent disk and realtime-memory reads for:

  • selected-key and shared-shredding MAP access;
  • nested projections;
  • shredded VARIANT access and binary fallback.

Tests

Added or updated coverage for:

  • external and sparse realtime offsets;
  • invalid, duplicate, backward, and overflowing offsets;
  • commit progress, snapshot offsets, and recovery;
  • exact row counting with sparse offsets;
  • append-only and primary-key realtime reads;
  • MAP and VARIANT projections across disk and memory.

API and Format

Realtime write input and RealtimeStore APIs are updated to support external offsets and shared read processing.
Data-file and snapshot offset-file formats are unchanged.

Documentation

Generative AI tooling

Generated-by: OpenAI Codex (GPT-5)

@lxy-9602 lxy-9602 changed the title refactor(realtime): move offset handling to Paimon readers feat(realtime): support shredding-aware realtime reads & refactor rt read Sep 1, 2026
@lxy-9602 lxy-9602 changed the title feat(realtime): support shredding-aware realtime reads & refactor rt read feat(realtime): support external offsets and shredding-aware reads Sep 1, 2026
auto offset_iter = merged_offsets.find(partition_bucket);
int64_t previous_end_offset = offset_iter == merged_offsets.end() ? 0 : offset_iter->second;
if (offset_range.begin != previous_end_offset) {
if (offset_range.begin < previous_end_offset) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this skip an earlier prepared segment?\n\nFor example, if P0 contains offset 10 and P1 contains offset 30, committing only P1 advances the watermark to 31. P0 may then be reclaimed and later treated as already committed, although its files were never published.\n\nI think row offsets can remain sparse, but commit progress should still be contiguous.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. This can happen if the caller passes only P1, but we consider that invalid input to CommitWithProgress.

The range here describes the envelope of actual row offsets, which may be sparse. Therefore, the committer cannot distinguish a legitimate offset gap from an omitted prepared segment, and requiring numeric continuity would reject valid sparse offsets.

Streaming engines generally enforce this at the checkpoint coordinator layer. For example, Flink’s checkpoint-subsuming contract requires a later completed checkpoint to include all artifacts from earlier uncommitted checkpoints. The sink coordinator retains prepared-but-uncommitted committables and submits the complete pending prefix when committing a later checkpoint. Low-level offset APIs such as Kafka also trust the caller to commit the next valid consumption position rather than validating that every preceding record was processed.

We will keep sparse ranges supported and document that the upstream coordinator must provide all earlier prepared-but-uncommitted progress for each partition-bucket. Omitting an earlier prepared segment violates the API contract and may cause its unpublished data to be treated as committed and reclaimed.

Status RealtimePrimaryKeyWriter::FlushSegment(
const std::shared_ptr<RealtimeSegmentHandle>& segment) {
PAIMON_ASSIGN_OR_RAISE(std::vector<std::unique_ptr<BatchReader>> readers,
realtime_store_->CreateCommitReaders(segment));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we validate the raw PK commit readers before MOR?

This path no longer checks segment->GetRowCount(). If a custom store misses or duplicates a mutation, deduplication may hide it and incomplete data could be committed.

We should at least check the raw row count and validate that _REALTIME_OFFSET is non-null, unique, and within the sealed range. The same offset validation may also be useful for append.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that we can compare the total number of rows emitted by the raw commit readers with segment->GetRowCount(), consistent with the append path.

While I don’t think core should revalidate _REALTIME_OFFSET uniqueness or range here. RealtimeStore::CreateCommitReaders already requires implementations to expose every sealed row exactly once, similar to how core trusts other plugin (e.g., file-format readers) to return correct field values. Offset uniqueness would also require additional state because PK readers are sorted by key and sequence rather than offset.

Even comprehensive offset validation cannot prove that the plugin returned the correct row contents or associated each offset with the correct mutation. Extending validation in this direction would effectively make core revalidate the plugin’s entire output, which is inconsistent with the trust boundary used for other plugin. A custom store violating this contract should be treated as a plugin bug.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This boundary makes sense. The raw row-count check before MOR should be sufficient here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that validating the raw row count would provide useful defense in depth. However, there is no simple reliable check at this layer with the current streaming reader contract.

Unlike append, PK commit readers are multiple one-shot streams consumed through MOR. The row count after MOR is not comparable because deduplication may reduce it. Checking before MOR would require wrapping every raw reader with shared counting state, detecting completion across all reader EOFs, and propagating a mismatch before the rolling file writer completes. Pre-reading would instead require buffering or reopening the streams.

Since CreateCommitReaders already requires plugins to expose every sealed row exactly once, I will add a TODO/follow-up for cardinality validation.

Comment thread src/paimon/core/io/key_value_data_file_record_reader.cpp
Comment thread src/paimon/core/realtime/realtime_primary_key_reader.cpp
Comment thread include/paimon/realtime/offset_range.h Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants